#!/usr/bin/env vpython3
#
# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Helps generate enums.xml from ProductionSupportedFlagList.

This is only a best-effort attempt to generate enums.xml values for the
LoginCustomFlags enum. You need to verify this script picks the right string
value for the new features and double check the hash value by running
"AboutFlagsHistogramTest.*".
"""

from __future__ import print_function

import argparse
import os
import re
import hashlib
import ctypes
import xml.etree.ElementTree as ET
import logging
import sys

_CHROMIUM_SRC = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)
sys.path.append(os.path.join(_CHROMIUM_SRC, 'third_party', 'catapult', 'devil'))
from devil.utils import logging_common  # pylint: disable=wrong-import-position

_FLAG_LIST_FILE = os.path.join(_CHROMIUM_SRC, 'android_webview', 'java', 'src',
                               'org', 'chromium', 'android_webview', 'common',
                               'ProductionSupportedFlagList.java')
_ENUMS_XML_FILE = os.path.join(_CHROMIUM_SRC, 'tools', 'metrics', 'histograms',
                               'enums.xml')

# This script tries to guess the commandline switch/base::Feature name from the
# generated Java constant (assuming the constant name follows typical
# conventions), but sometimes the script generates the incorrect name.
# In this case, you can teach the
# script the right name is by editing this dictionary. The perk of editing
# here instead of fixing enums.xml by hand is this script *should* generate the
# correct hash value once you add the right name, so you can just rerun the
# script to get the correct set of enum entries.
#
# Keys are the names autogenerated by this script's logic, values are the
# base::Feature/switch string names as they would appear in Java/C++ code.
KNOWN_MISTAKES = {
    # 'AutogeneratedName': 'CorrectName',
    'WebViewAccelerateSmallCanvases': 'WebviewAccelerateSmallCanvases',
    'EnableSharedImageForWebView': 'EnableSharedImageForWebview',
    'GmsCoreEmoji': 'GMSCoreEmoji',
    'KeyboardAccessoryPaymentVirtualCardFeature': 'IPH_KeyboardAccessoryPaymentVirtualCard',  # pylint: disable=line-too-long
    'CompositeBgColorAnimation': 'CompositeBGColorAnimation',
    'RtcDisallowPlanBOutsideDeprecationTrial': 'RTCDisallowPlanBOutsideDeprecationTrial',  # pylint: disable=line-too-long
}


def GetSwitchId(label):
  """Generate a hash consistent with flags_ui::GetSwitchUMAId()."""
  digest = hashlib.md5(label.encode('utf-8')).hexdigest()
  first_eight_bytes = digest[:16]
  long_value = int(first_eight_bytes, 16)
  signed_32bit = ctypes.c_int(long_value).value
  return signed_32bit


def _Capitalize(value):
  value = value[0].upper() + value[1:].lower()
  if value == 'Webview':
    value = 'WebView'
  return value


def FormatName(name, convert_to_pascal_case):
  """Converts name to the correct format.

  If name is shouty-case (ex. 'SOME_NAME') like a Java constant, then:
    * it converts to pascal case (camel case, with the first letter capitalized)
      if convert_to_pascal_case == True (ex. 'SomeName')
    * it converts to hyphenates name and converts to lower case (ex.
      'some-name')
  raises
    ValueError if name contains quotation marks like a Java literal (ex.
      '"SomeName"')
  """
  has_quotes_re = re.compile(r'".*"')
  if has_quotes_re.match(name):
    raise ValueError('String literals are not supported (got {})'.format(name))
  name = re.sub(r'^[^.]+\.', '', name)
  sections = name.split('_')

  if convert_to_pascal_case:
    sections = [_Capitalize(section) for section in sections]
    return ''.join(sections)

  sections = [section.lower() for section in sections]
  return '-'.join(sections)


def ConvertNameIfNecessary(name):
  """Fixes any names which are known to be autogenerated incorrectly."""
  if name in KNOWN_MISTAKES.keys():
    return KNOWN_MISTAKES.get(name)
  return name


class Flag:
  """Simplified python equivalent of the Flag java class.

  See //android_webview/java/src/org/chromium/android_webview/common/Flag.java
  """

  def __init__(self, name, is_base_feature):
    self.name = name
    self.is_base_feature = is_base_feature


class EnumValue:
  def __init__(self, label):
    self.label = label
    self.value = GetSwitchId(label)

  def ToXml(self):
    return '<int value="{value}" label="{label}"/>'.format(value=self.value,
                                                           label=self.label)


def _GetExistingFlagLabels():
  with open(_ENUMS_XML_FILE) as f:
    root = ET.fromstring(f.read())
  all_enums = root.find('enums')
  login_custom_flags = all_enums.find('enum[@name="LoginCustomFlags"]')
  return [item.get('label') for item in login_custom_flags]


def _RemoveDuplicates(enums, existing_labels):
  return [enum for enum in enums if enum.label not in existing_labels]


def ExtractFlagsFromJavaLines(lines):
  flags = []

  hanging_name_re = re.compile(
      r'(?:\s*Flag\.(?:baseFeature|commandLine)\()?(\S+),')
  pending_feature = False
  pending_commandline = False

  for line in lines:
    if 'baseFeature(' in line:
      pending_feature = True
    if 'commandLine(' in line:
      pending_commandline = True

    if pending_feature and pending_commandline:
      raise RuntimeError('Somehow this is both a baseFeature and commandLine '
                         'switch: ({})'.format(line))

    # This means we saw Flag.baseFeature() or Flag.commandLine() on this or a
    # previous line but haven't found that flag's name yet. Check if we can
    # find a name in this line.
    if pending_feature or pending_commandline:
      m = hanging_name_re.search(line)
      if m:
        name = m.group(1)
        try:
          formatted_name = FormatName(name, pending_feature)
          formatted_name = ConvertNameIfNecessary(formatted_name)
          flags.append(Flag(formatted_name, pending_feature))
          pending_feature = False
          pending_commandline = False
        except ValueError:
          logging.warning('String literals are not supported, skipping %s',
                          name)
  return flags


def _GetMissingWebViewEnums():
  with open(_FLAG_LIST_FILE, 'r') as f:
    lines = f.readlines()
  flags = ExtractFlagsFromJavaLines(lines)

  enums = []
  for flag in flags:
    if flag.is_base_feature:
      enums.append(EnumValue(flag.name + ':enabled'))
      enums.append(EnumValue(flag.name + ':disabled'))
    else:
      enums.append(EnumValue(flag.name))

  existing_labels = set(_GetExistingFlagLabels())
  enums_to_add = _RemoveDuplicates(enums, existing_labels)
  return enums_to_add


def CheckMissingWebViewEnums(input_api, output_api):
  """A presubmit check to find missing flag enums."""
  sources = input_api.AffectedSourceFiles(
      lambda affected_file: input_api.FilterSourceFile(
          affected_file,
          files_to_check=(r'.*\bProductionSupportedFlagList\.java$', )))
  if not sources:
    return []

  enums_to_add = _GetMissingWebViewEnums()
  if not enums_to_add:
    return []

  script_path = '//android_webview/tools/PRESUBMIT.py'
  enums_path = '//tools/metrics/histograms/enums.xml'
  xml_strs = sorted(['  ' + enum.ToXml() for enum in enums_to_add])

  return [
      output_api.PresubmitPromptWarning("""
It looks like new flags have been added to ProductionSupportedFlagList but the
labels still need to be added to LoginCustomFlags enum in {enums_path}.
If you believe this
warning is correct, please update enums.xml by pasting the following lines under
LoginCustomFlags and running `git-cl format` to correctly sort the changes:

{xml_strs}

You can run this check again by running the {script_path} tool.

If you believe this warning is a false positive, you can silence this warning by
updating KNOWN_MISTAKES in {script_path}.
""".format(xml_strs='\n'.join(xml_strs),
           enums_path=enums_path,
           script_path=script_path))
  ]


def main():
  parser = argparse.ArgumentParser()

  logging_common.AddLoggingArguments(parser)
  args = parser.parse_args()
  logging_common.InitializeLogging(args)

  enums_to_add = _GetMissingWebViewEnums()

  xml_strs = sorted(['  ' + enum.ToXml() for enum in enums_to_add])
  if not xml_strs:
    print('enums.xml is already up-to-date!')
    return

  message = """\
This is a best-effort attempt to generate missing enums.xml entries. Please
double-check this picked the correct labels for your new features (labels are
case-sensitive!), add these to enums.xml, run `git-cl format`, and then follow
these steps as a final check:

https://chromium.googlesource.com/chromium/src/+/main/tools/metrics/histograms/README.md#flag-histograms

If any labels were generated incorrectly, please edit this script and change
KNOWN_MISTAKES.
"""
  print(message)

  for xml_str in xml_strs:
    print(xml_str)


if __name__ == '__main__':
  main()
